=head1 NAME

  RTx::AssetTracker::Assets - a collection of AssetTracker Assets objects

=head1 SYNOPSIS

  use RTx::AssetTracker::Assets;

=head1 DESCRIPTION


=head1 METHODS

=begin testing 

use RTx::AssetTracker::Assets;

=end testing

=cut


package RTx::AssetTracker::Assets;

use strict;
no warnings qw(redefine);
use vars qw(@SORTFIELDS);
use RTx::AssetTracker::CustomFields;

# FIELDS is a mapping of searchable Field name, to Type, and other
# metadata.

my %FIELDS =
  ( Name            => ['STRING',],
    Status          => ['ENUM'],
    Type            => ['ENUM' => 'Type',],
    Creator         => ['ENUM' => 'User',],
    LastUpdatedBy   => ['ENUM' => 'User',],
    Owner           => ['ENUM' => 'Group',],
    id              => ['INT',],
    Parent          => ['INT',],
    LastUpdated     => ['DATE' => 'LastUpdated',],
    Created         => ['DATE' => 'Created',],
    Description     => ['STRING',],
    Content         => ['TRANSFIELD',],
    ContentType     => ['TRANSFIELD',],
    Filename        => ['TRANSFIELD',],
    IP              => ['IPFIELD',],
    MAC             => ['IPFIELD',],
    Interface       => ['IPFIELD',],
    TransactionDate => ['TRANSDATE',],
    Updated => ['TRANSDATE',],
    Owner           => ['WATCHERFIELD' => 'Owner',],
    Admin           => ['WATCHERFIELD' => 'Admin',],
    Watcher         => ['WATCHERFIELD'],
    LinkedTo        => ['LINKFIELD',],
    CustomFieldValue =>['CUSTOMFIELD',],
    CF              => ['CUSTOMFIELD',],
    OwnerGroup      => ['MEMBERSHIPFIELD' => 'Owner',],
    AdminGroup      => ['MEMBERSHIPFIELD' => 'Admin',],
    WatcherGroup    => ['MEMBERSHIPFIELD',],
  );
# Mapping of Field Type to Function
my %dispatch =
  ( ENUM            => \&_EnumLimit,
    INT             => \&_IntLimit,
    LINK            => \&_LinkLimit,
    DATE            => \&_DateLimit,
    STRING          => \&_StringLimit,
    TRANSFIELD      => \&_TransLimit,
    IPFIELD         => \&_IPLimit,
    TRANSDATE       => \&_TransDateLimit,
    WATCHERFIELD    => \&_WatcherLimit,
    MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
    LINKFIELD       => \&_LinkFieldLimit,
    CUSTOMFIELD    => \&_CustomFieldLimit,
  );
my %can_bundle =
  ( WATCHERFIELD => "yeps",
  );

# Default EntryAggregator per type
# if you specify OP, you must specify all valid OPs
my %DefaultEA = (
                 INT            => 'AND',
                 ENUM           => { '=' => 'OR',
                                     '!='=> 'AND'
                                   },
                 DATE           => { '=' => 'OR',
                                     '>='=> 'AND',
                                     '<='=> 'AND',
                                     '>' => 'AND',
                                     '<' => 'AND'
                                   },
                 STRING         => { '=' => 'OR',
                                     '!='=> 'AND',
                                     'LIKE'=> 'AND',
                                     'NOT LIKE' => 'AND'
                                   },
                 TRANSFIELD     => 'AND',
                 IPFIELD        => 'OR',
                 TRANSDATE      => 'AND',
                 LINK           => 'OR',
                 LINKFIELD      => 'AND',
                 TARGET         => 'AND',
                 BASE           => 'AND',
                 WATCHERFIELD   => { '=' => 'OR',
                                     '!='=> 'AND',
                                     'LIKE'=> 'OR',
                                     'NOT LIKE' => 'AND'
                                   },

                 CUSTOMFIELD    => 'OR',
                );
# Helper functions for passing the above lexically scoped tables above
# into Tickets_Overlay_SQL.
sub FIELDS   { return \%FIELDS   }
sub dispatch { return \%dispatch }
sub can_bundle { return \%can_bundle }

# {{{ sub SortFields

@SORTFIELDS = qw(id Status
                 Type Subject
         Owner Created Due Starts Started
         Told
                 Resolved LastUpdated Priority TimeWorked TimeLeft);

=head2 SortFields

Returns the list of fields that lists of assets can easily be sorted by

=cut

sub SortFields {
        my $self = shift;
        return(@SORTFIELDS);
}


# }}}


# BEGIN SQL STUFF *********************************

=head1 Limit Helper Routines

These routines are the targets of a dispatch table depending on the
type of field.  They all share the same signature:

  my ($self,$field,$op,$value,@rest) = @_;

The values in @rest should be suitable for passing directly to
DBIx::SearchBuilder::Limit.

Essentially they are an expanded/broken out (and much simplified)
version of what ProcessRestrictions used to do.  They're also much
more clearly delineated by the TYPE of field being processed.

=head2 _EnumLimit

Handle Fields which are limited to certain values, and potentially
need to be looked up from another class.

This subroutine actually handles two different kinds of fields.  For
some the user is responsible for limiting the values.  (i.e. Status,
Type).

For others, the value specified by the user will be looked by via
specified class.

Meta Data:
  name of class to lookup in (Optional)

=cut

sub _EnumLimit {
  my ($sb,$field,$op,$value,@rest) = @_;

  # SQL::Statement changes != to <>.  (Can we remove this now?)
  $op = "!=" if $op eq "<>";

  die "Invalid Operation: $op for $field"
    unless $op eq "=" or $op eq "!=";

  my $meta = $FIELDS{$field};
  if (defined $meta->[1]) {
    my $class = "RTx::AssetTracker::" . $meta->[1];
    my $o = $class->new($sb->CurrentUser);
    $o->Load( $value );
    $value = $o->Id;
  }
  $sb->_SQLLimit( FIELD => $field,
              VALUE => $value,
              OPERATOR => $op,
              @rest,
            );
}

=head2 _IntLimit

Handle fields where the values are limited to integers.  (For example,
Priority, TimeWorked.)

Meta Data:
  None

=cut

sub _IntLimit {
  my ($sb,$field,$op,$value,@rest) = @_;

  die "Invalid Operator $op for $field"
    unless $op =~ /^(=|!=|>|<|>=|<=)$/;

  $sb->_SQLLimit(
             FIELD => $field,
             VALUE => $value,
             OPERATOR => $op,
             @rest,
            );
}


=head2 _DateLimit

Handle date fields.  (Created, LastTold..)

Meta Data:
  1: type of link.  (Probably not necessary.)

=cut

sub _DateLimit {
  my ($sb,$field,$op,$value,@rest) = @_;

  die "Invalid Date Op: $op"
     unless $op =~ /^(=|>|<|>=|<=)$/;

  my $meta = $FIELDS{$field};
  die "Incorrect Meta Data for $field"
    unless (defined $meta->[1]);

  require Time::ParseDate;
  use POSIX 'strftime';

  # FIXME: Replace me with RT::Date( Type => 'unknown' ...)
  my $time = Time::ParseDate::parsedate( $value,
                        UK => $RT::DateDayBeforeMonth,
                        PREFER_PAST => $RT::AmbiguousDayInPast,
                        PREFER_FUTURE => !($RT::AmbiguousDayInPast),
                        FUZZY => 1
                                       );

  if ($op eq "=") {
    # if we're specifying =, that means we want everything on a
    # particular single day.  in the database, we need to check for >
    # and < the edges of that day.

    my $daystart = strftime("%Y-%m-%d %H:%M",
                            gmtime($time - ( $time % 86400 )));
    my $dayend   = strftime("%Y-%m-%d %H:%M",
                            gmtime($time + ( 86399 - $time % 86400 )));

    $sb-> _OpenParen;

    $sb->_SQLLimit(
                   FIELD => $meta->[1],
                   OPERATOR => ">=",
                   VALUE => $daystart,
                   @rest,
                  );

    $sb->_SQLLimit(
                   FIELD => $meta->[1],
                   OPERATOR => "<=",
                   VALUE => $dayend,
                   @rest,
                   ENTRYAGGREGATOR => 'AND',
                  );

    $sb-> _CloseParen;

  } else {
    $value = strftime("%Y-%m-%d %H:%M", gmtime($time));
    $sb->_SQLLimit(
                   FIELD => $meta->[1],
                   OPERATOR => $op,
                   VALUE => $value,
                   @rest,
                  );
  }
}

=head2 _StringLimit

Handle simple fields which are just strings.  (Subject,Type)

Meta Data:
  None

=cut

sub _StringLimit {
  my ($sb,$field,$op,$value,@rest) = @_;

  # FIXME:
  # Valid Operators:
  #  =, !=, LIKE, NOT LIKE

  $sb->_SQLLimit(
             FIELD => $field,
             OPERATOR => $op,
             VALUE => $value,
             CASESENSITIVE => 0,
             @rest,
            );
}

=head2 _IPLimit

Handle fields limiting based on IP

Meta Data:
  None

=cut

sub _IPLimit {

  my ($sb,$field,$op,$value,@rest) = @_;

  # See the comments for TransLimit, they apply here too

  $sb->{_sql_ipalias} = $sb->NewAlias ('AT_IPs')
    unless defined $sb->{_sql_ipalias};

  #$sb->_OpenParen;

  # Join IPs to Assets
  $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
             ALIAS2 => $sb->{_sql_ipalias}, FIELD2 => 'Asset');

  $sb->Limit( ALIAS => $sb->{_sql_ipalias}, FIELD => $field, OPERATOR => $op, VALUE => $value, @rest);

  #$sb->_CloseParen;
}

=head2 _TransDateLimit

Handle fields limiting based on Transaction Date.

The inpupt value must be in a format parseable by Time::ParseDate

Meta Data:
  None

=cut

# This routine should really be factored into translimit.
sub _TransDateLimit {
  my ($sb,$field,$op,$value,@rest) = @_;

  # See the comments for TransLimit, they apply here too

  $sb->{_sql_transalias} = $sb->NewAlias ('Transactions')
    unless defined $sb->{_sql_transalias};
  $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments')
    unless defined $sb->{_sql_trattachalias};


  # Join Transactions To Attachments
  $sb->_OpenParen;

  #Search for the right field
  $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
                 FIELD =>    'Created',
                 OPERATOR => $op,
                 VALUE =>    $value,
                 CASESENSITIVE => 0,
                 @rest
                );

  $sb->_SQLJoin( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
             ALIAS2 => $sb->{_transalias}, FIELD2 => 'id');

  # Join Transactions to Tickets
  $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
             ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'ObjectId');

  $sb->Limit( ALIAS => $sb->{_sql_transalias}, FIELD => 'ObjectType', VALUE => 'RTx::AssetTracker::Asset');


  my $d = new RT::Date( $sb->CurrentUser );
  $d->Set( Format => 'ISO', Value => $value);
   $value = $d->ISO;

  $sb->_CloseParen;
}

=head2 _TransLimit

Limit based on the Content of a transaction or the ContentType.

Meta Data:
  none

=cut

sub _TransLimit {
  # Content, ContentType, Filename

  # If only this was this simple.  We've got to do something
  # complicated here:

            #Basically, we want to make sure that the limits apply to
            #the same attachment, rather than just another attachment
            #for the same ticket, no matter how many clauses we lump
            #on. We put them in TicketAliases so that they get nuked
            #when we redo the join.

  # In the SQL, we might have
  #       (( Content = foo ) or ( Content = bar AND Content = baz ))
  # The AND group should share the same Alias.

  # Actually, maybe it doesn't matter.  We use the same alias and it
  # works itself out? (er.. different.)

  # Steal more from _ProcessRestrictions

  # FIXME: Maybe look at the previous FooLimit call, and if it was a
  # TransLimit and EntryAggregator == AND, reuse the Aliases?

  # Or better - store the aliases on a per subclause basis - since
  # those are going to be the things we want to relate to each other,
  # anyway.

  # maybe we should not allow certain kinds of aggregation of these
  # clauses and do a psuedo regex instead? - the problem is getting
  # them all into the same subclause when you have (A op B op C) - the
  # way they get parsed in the tree they're in different subclauses.

  my ($self,$field,$op,$value,@rest) = @_;

  $self->{_sql_transalias} = $self->NewAlias ('Transactions')
    unless defined $self->{_sql_transalias};
  $self->{_sql_trattachalias} = $self->NewAlias ('Attachments')
    unless defined $self->{_sql_trattachalias};


    $self->_OpenParen;

  #Search for the right field
  $self->_SQLLimit(ALIAS => $self->{_sql_trattachalias},
                 FIELD =>    $field,
                 OPERATOR => $op,
                 VALUE =>    $value,
                 CASESENSITIVE => 0,
                 @rest
                );


  $self->_SQLJoin( ALIAS1 => $self->{_sql_trattachalias}, FIELD1 => 'TransactionId',
             ALIAS2 => $self->{_sql_transalias}, FIELD2 => 'id');

  # Join Transactions to Tickets
  $self->_SQLJoin( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'}, # Why not use "id" here?
             ALIAS2 => $self->{_sql_transalias}, FIELD2 => 'ObjectId');

    $self->Limit( ALIAS => $self->{_sql_transalias}, FIELD => 'ObjectType', VALUE => 'RTx::AssetTracker::Asset', ENTRYAGGREGATOR => 'AND');


    $self->_CloseParen;

}

=head2 _WatcherLimit

Handle watcher limits.  (Requestor, CC, etc..)

Meta Data:
  1: Field to query on


=begin testing


=end testing

=cut

sub _WatcherLimit {
    my $self  = shift;
    my $field = shift;
    my $op    = shift;
    my $value = shift;
    my %rest  = (@_);

    $self->_OpenParen;

    # Find out what sort of watcher we're looking for
    my $fieldname;
    if (ref $field) {
        $fieldname = $field->[0]->[0];
    } else {
        $fieldname = $field;
    }
    my $meta = $FIELDS{$fieldname};
    my $type = ( defined $meta->[1] ? $meta->[1] : undef );


    # We only want _one_ clause for all of requestors, cc, admincc
    # It's less flexible than what we used to do, but now it sort of actually works. (no huge cartesian products that hose the db)
    my $groups       = $self->{'watcherlimit_'.('global')."_groups"} ||=  $self->NewAlias('Groups');
    my $groupmembers = $self->{'watcherlimit_'.('global')."_groupmembers"} ||=  $self->NewAlias('CachedGroupMembers');
    my $users        = $self->{'watcherlimit_'.('global')."_users"} ||= $self->NewAlias('Users');


    # Use regular joins instead of SQL joins since we don't want the joins inside ticketsql or we get a huge cartesian product
    $self->Limit( ALIAS => $groups, FIELD => 'Domain', VALUE => 'RT::Ticket-Role', ENTRYAGGREGATOR => 'AND');
    $self->Join( ALIAS1 => $groups, FIELD1 => 'Instance', ALIAS2 => 'main', FIELD2 => 'id');
    $self->Join( ALIAS1 => $groups, FIELD1 => 'id', ALIAS2 => $groupmembers, FIELD2 => 'GroupId');
    $self->Join( ALIAS1 => $groupmembers, FIELD1 => 'MemberId', ALIAS2 => $users, FIELD2 => 'id');


    # If we're looking for multiple watchers of a given type,
    # TicketSQL will be handing it to us as an array of clauses in
    # $field
    if ( ref $field ) {    # gross hack
        $self->_OpenParen;
        for my $chunk (@$field) {
            ( $field, $op, $value, %rest ) = @$chunk;
            $self->_SQLLimit(
                ALIAS         => $users,
                FIELD         => $rest{SUBKEY} || 'EmailAddress',
                VALUE         => $value,
                OPERATOR      => $op,
                CASESENSITIVE => 0,
                %rest);
        }
        $self->_CloseParen;
    }
    else {
        $self->_SQLLimit(
            ALIAS         => $users,
            FIELD         => $rest{SUBKEY} || 'EmailAddress',
            VALUE         => $value,
            OPERATOR      => $op,
            CASESENSITIVE => 0,
            %rest);
    }

    $self->_SQLLimit( ALIAS => $groups, FIELD => 'Type', VALUE => $type, ENTRYAGGREGATOR => 'AND') if ($type);

    $self->_CloseParen;
}

=head2 _WatcherMembershipLimit

Handle watcher membership limits, i.e. whether the watcher belongs to a
specific group or not.

Meta Data:
  1: Field to query on

SELECT DISTINCT main.*
FROM
    Tickets main,
    Groups Groups_1,
    CachedGroupMembers CachedGroupMembers_2,
    Users Users_3
WHERE (
    (main.EffectiveId = main.id)
) AND (
    (main.Status != 'deleted')
) AND (
    (main.Type = 'ticket')
) AND (
    (
        (Users_3.EmailAddress = '22')
            AND
        (Groups_1.Domain = 'RT::Ticket-Role')
            AND
        (Groups_1.Type = 'RequestorGroup')
    )
) AND
    Groups_1.Instance = main.id
AND
    Groups_1.id = CachedGroupMembers_2.GroupId
AND
    CachedGroupMembers_2.MemberId = Users_3.id
ORDER BY main.id ASC
LIMIT 25
=cut

sub _WatcherMembershipLimit {
  my ($self,$field,$op,$value,@rest) = @_;
  my %rest = @rest;

  $self->_OpenParen;

  my $groups        = $self->NewAlias('Groups');
  my $groupmembers  = $self->NewAlias('CachedGroupMembers');
  my $users         = $self->NewAlias('Users');
  my $memberships   = $self->NewAlias('CachedGroupMembers');

  if (ref $field) { # gross hack
    my @bundle = @$field;
    $self->_OpenParen;
    for my $chunk (@bundle) {
      ($field,$op,$value,@rest) = @$chunk;
      $self->_SQLLimit(ALIAS => $memberships,
                   FIELD => 'GroupId',
                   VALUE           => $value,
                   OPERATOR        => $op,
                   @rest,
                  );
    }
    $self->_CloseParen;
  } else {
     $self->_SQLLimit(ALIAS => $memberships,
                   FIELD => 'GroupId',
                   VALUE           => $value,
                   OPERATOR        => $op,
                   @rest,
                  );
  }

  # {{{ Tie to groups for assets we care about
  $self->_SQLLimit(ALIAS => $groups,
                   FIELD => 'Domain',
                   VALUE => 'RTx::AssetTracker::Asset-Role',
                   ENTRYAGGREGATOR => 'AND');

  $self->Join(ALIAS1 => $groups, FIELD1 => 'Instance',
              ALIAS2 => 'main',   FIELD2 => 'id');
  # }}}

  # If we care about which sort of watcher
  my $meta = $FIELDS{$field};
  my $type = ( defined $meta->[1] ? $meta->[1] : undef );

  if ( $type ) {
    $self->_SQLLimit(ALIAS => $groups,
                     FIELD => 'Type',
                     VALUE => $type,
                     ENTRYAGGREGATOR => 'AND');
  }

  $self->Join (ALIAS1 => $groups,  FIELD1 => 'id',
               ALIAS2 => $groupmembers, FIELD2 => 'GroupId');

  $self->Join( ALIAS1 => $groupmembers, FIELD1 => 'MemberId',
               ALIAS2 => $users, FIELD2 => 'id');

  $self->Join( ALIAS1 => $memberships, FIELD1 => 'MemberId',
               ALIAS2 => $users, FIELD2 => 'id');

 $self->_CloseParen;

}


sub _LinkFieldLimit {
  my $restriction;
  my $self;
  my $LinkAlias;
  my %args;
  if ($restriction->{'TYPE'}) {
    $self->SUPER::Limit(ALIAS => $LinkAlias,
                        ENTRYAGGREGATOR => 'AND',
                        FIELD =>   'Type',
                        OPERATOR => '=',
                        VALUE =>    $restriction->{'TYPE'} );
  }

   #If we're trying to limit it to things that are target of
  if ($restriction->{'TARGET'}) {
    # If the TARGET is an integer that means that we want to look at
    # the LocalTarget field. otherwise, we want to look at the
    # "Target" field
    my ($matchfield);
    if ($restriction->{'TARGET'} =~/^(\d+)$/) {
      $matchfield = "LocalTarget";
    } else {
      $matchfield = "Target";
    }
    $self->SUPER::Limit(ALIAS => $LinkAlias,
                        ENTRYAGGREGATOR => 'AND',
                        FIELD =>   $matchfield,
                        OPERATOR => '=',
                        VALUE =>    $restriction->{'TARGET'} );
    #If we're searching on target, join the base to asset.id
    $self->_SQLJoin( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
                 ALIAS2 => $LinkAlias,
                 FIELD2 => 'LocalBase');
  }
  #If we're trying to limit it to things that are base of
  elsif ($restriction->{'BASE'}) {
    # If we're trying to match a numeric link, we want to look at
    # LocalBase, otherwise we want to look at "Base"
    my ($matchfield);
    if ($restriction->{'BASE'} =~/^(\d+)$/) {
      $matchfield = "LocalBase";
    } else {
      $matchfield = "Base";
    }

    $self->SUPER::Limit(ALIAS => $LinkAlias,
                        ENTRYAGGREGATOR => 'AND',
                        FIELD => $matchfield,
                        OPERATOR => '=',
                        VALUE =>    $restriction->{'BASE'} );
    #If we're searching on base, join the target to asset.id
    $self->_SQLJoin( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
                 ALIAS2 => $LinkAlias,
                 FIELD2 => 'LocalTarget')
  }
}


=head2 KeywordLimit

Limit based on Keywords

Meta Data:
  none

=cut

sub _CustomFieldLimit {
    my ( $self, $_field, $op, $value, @rest ) = @_;

  my %rest = @rest;
  my $field = $rest{SUBKEY} || die "No field specified";

  # For our sanity, we can only limit on one type at a time
  my $type = 0;

    if ( $field =~ /^(.+?)\.{(.+)}$/ ) {
    $type =  $1;
    $field = $2;
   }
    $field = $1 if $field =~ /^{(.+)}$/; # trim { }



# If we're trying to find custom fields that don't match something, we want assets
# where the custom field has no value at all

    my $null_columns_ok;
    if ( ( $op =~ /^IS$/i ) or ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) {
        $null_columns_ok = 1;
    }

    my $cfid = 0;
    if ($type) {

    my $q = RTx::AssetTracker::Type->new( $self->CurrentUser );
    $q->Load($type) if ($type);

    my $cf;
    if ( $q->id ) {
        $cf = $q->CustomField($field);
    }
    else {
        $cf = RTx::AssetTracker::CustomField->new( $self->CurrentUser );
        $cf->LoadByNameAndType( Type => '0', Name => $field );
    }

     $cfid = $cf->id;

    }


    my $AssetCFs;

  # Perform one Join per CustomField
    if ( $self->{_sql_object_cf_alias}{$cfid} ) {
    $AssetCFs = $self->{_sql_object_cf_alias}{$cfid};
  }
    else {
        $AssetCFs = $self->{_sql_object_cf_alias}{$cfid} = $self->Join(
            TYPE   => 'left',
            ALIAS1 => 'main',
            FIELD1 => 'id',
            TABLE2 => 'ObjectCustomFieldValues',
            FIELD2 => 'ObjectId'
        );

    $self->Limit(
        LEFTJOIN        => $AssetCFs,
        FIELD => 'ObjectType',
        VALUE => ref($self->NewItem), # we want a single item, not a collection
        ENTRYAGGREGATOR => 'AND'
    );

    if ($cfid) {
    $self->Limit(
        LEFTJOIN        => $AssetCFs,
        FIELD           => 'CustomField',
        VALUE           => $cfid,
        ENTRYAGGREGATOR => 'AND'
    );
    } else {
    my $cfalias = $self->Join(
        ALIAS1        => $AssetCFs,
        FIELD1           => 'CustomField',
        TABLE2          => 'CustomFields',
        FIELD2          => 'id'
    );
    $self->Limit(
        LEFTJOIN        => $cfalias,
        FIELD           => 'Name',
        VALUE           => $field,
    );


    }
    }

    $self->_OpenParen if ($null_columns_ok);

    $self->_SQLLimit(
        ALIAS      => $AssetCFs,
        FIELD      => 'Content',
        OPERATOR   => $op,
        VALUE      => $value,
        QUOTEVALUE => 1,
        @rest
    );
    if ($null_columns_ok) {
        $self->_SQLLimit(
            ALIAS           => $AssetCFs,
            FIELD           => 'Content',
            OPERATOR        => 'IS',
            VALUE           => 'NULL',
            QUOTEVALUE      => 0,
            ENTRYAGGREGATOR => 'OR',
        );
  }
    $self->_CloseParen if ($null_columns_ok);

}


# End Helper Functions

# End of SQL Stuff -------------------------------------------------



require RTx::AssetTracker::Assets_Overlay_SQL;



# {{{ sub _Init
sub _Init {
  my $self = shift;
  $self->{'table'} = "AT_Assets";
  $self->{'primary_key'} = "id";

  # By default, order by name
  $self->OrderBy( ALIAS => 'main',
                  FIELD => 'Name',
                  ORDER => 'ASC');

  return ($self->SUPER::_Init(@_));
}
# }}}

# {{{ sub Limit
sub Limit  {
  my $self = shift;
  my %args = ( ENTRYAGGREGATOR => 'AND',
               @_);
  $self->SUPER::Limit(%args);
}
# }}}

# {{{ sub Next
sub Next {
    my $self = shift;

    $self->_ProcessRestrictions() if ( $self->{'RecalcAssetLimits'} == 1 );

    my $Asset = $self->SUPER::Next();
    if ( ( defined($Asset) ) and ( ref($Asset) ) ) {

        #Make sure we _never_ show deleted assets
        #TODO we should be doing this in the where clause.
        #but you can't do multiple clauses on the same field just yet :/

        #if ( $Asset->__Value('Status') eq 'retired' ) {
            #return ( $self->Next() );
        #}

        # Since Asset could be granted with more rights instead
        # of being revoked, it's ok if queue rights allow
        # ShowAsset.  It seems need another query, but we have
        # rights cache in Principal::HasRight.
        #elsif ($Asset->TypeObj->CurrentUserHasRight('ShowAsset')
        if ($Asset->TypeObj->CurrentUserHasRight('ShowAsset')
            || $Asset->CurrentUserHasRight('ShowAsset') )
        {
            return ($Asset);
        }

        #If the user doesn't have the right to show this asset
        else {
            return ( $self->Next() );
        }
    }

    #if there never was any ticket
    else {
        return (undef);
    }

}

# }}}


# {{{ Limit by enum or foreign key

# {{{ sub LimitType

=head2 LimitType

LimitType takes a paramhash with the fields OPERATOR and VALUE.
OPERATOR is one of = or !=. (It defaults to =).
VALUE is a queue id or Name.


=cut

sub LimitType {
    my $self = shift;
    my %args = (VALUE => undef,
                OPERATOR => '=',
                @_);

    #TODO  VALUE should also take queue names and queue objects
    #TODO FIXME why are we canonicalizing to name, not id, robrt?
    if ($args{VALUE} =~ /^\d+$/) {
      my $type = new RTx::AssetTracker::Type($self->CurrentUser);
      $type->Load($args{'VALUE'});
      $args{VALUE} = $type->Name;
    }

    # What if they pass in an Id?  Check for isNum() and convert to
    # string.

    #TODO check for a valid queue here

    $self->Limit (FIELD => 'Type',
                  VALUE => $args{VALUE},
                  OPERATOR => $args{'OPERATOR'},
                  DESCRIPTION => join(
                   ' ', $self->loc('Type'), $args{'OPERATOR'}, $args{VALUE},
                  ),
                 );

}
# }}}

# {{{ sub LimitId

=head2 LimitId

Takes a paramhash with the fields OPERATOR and VALUE.
OPERATOR is one of =, >, < or !=.
VALUE is a asset Id to search for

=cut

sub LimitId {
    my $self = shift;
    my %args = (OPERATOR => '=',
                @_);

    $self->Limit (FIELD => 'id',
                  VALUE => $args{'VALUE'},
                   OPERATOR => $args{'OPERATOR'},
                   DESCRIPTION => join(
                   ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'},
                  ),
                 );
}

# }}}

# {{{ sub LimitParent

=head2 LimitParent

Takes a paramhash with the fields OPERATOR and VALUE.
OPERATOR is one of =, >, < or !=.
VALUE is a asset Parent to search for

=cut

sub LimitParent {
    my $self = shift;
    my %args = (OPERATOR => '=',
                @_);

    $self->Limit (FIELD => 'Parent',
                  VALUE => $args{'VALUE'},
                   OPERATOR => $args{'OPERATOR'},
                   DESCRIPTION => join(
                   ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'},
                  ),
                 );
}

# }}}


# }}}

=head2 PrepForSerialization

You don't want to serialize a big assets object, as the {items} hash will be instantly invalid _and_ eat lots of space

=cut


sub PrepForSerialization {
    my $self = shift;
    delete $self->{'items'};
    $self->RedoSearch();
}

=head2 _BuildItemMap

    # Build up a map of first/last/next/prev items, so that we can display search nav quickly

=cut

sub _BuildItemMap {
    my $self = shift;

    my $items = $self->ItemsArrayRef;
    my $prev = 0 ;

    delete $self->{'item_map'};
    if ($items->[0]) {
        $self->{'item_map'}->{'first'} = $items->[0]->Id;
        while (my $item = shift @$items ) {
            my $id = $item->Id;
            $self->{'item_map'}->{$id}->{'defined'} = 1;
            $self->{'item_map'}->{$id}->{prev}  = $prev;
            $self->{'item_map'}->{$id}->{next}  = $items->[0]->Id if ($items->[0]);
            $prev = $id;
        }
        $self->{'item_map'}->{'last'} = $prev;
    }
}


=head2 ItemMap

Returns an a map of all items found by this search. The map is of the form

$ItemMap->{'first'} = first assetid found
$ItemMap->{'last'} = last assetid found
$ItemMap->{$id}->{prev} = the asset id found before $id
$ItemMap->{$id}->{next} = the asset id found after $id

=cut

sub ItemMap {
    my $self = shift;
    $self->_BuildItemMap() unless ($self->{'items_array'} and $self->{'item_map'});
    return ($self->{'item_map'});
}



1;
